//
// Created by momolove on 22-12-2.
//

#include "cfserver.h"

int main(int argc, char* argv[]) {
    init();
    parse_args(argc, argv);
    do_fserver();
}

void init() { memset(indent, ' ', MAX_INDENT); indent[MAX_INDENT] = '\0'; }

void print_help_and_exit(const char *arg0) {
    fprintf(stdout, "Usage: %s [OPTION] ... <DIR>\n"
           "Serve some files over TCP\n"
           "\n"
           "args:\n"
           "    <DIR> which directory to serve\n"
           "\n"
           "options:\n"
           "    -l, --listen=ADDR   specify source address to use [default is localhost]\n"
           "    -H, --hidden        show hidden file\n"
           "    -p, --port=PORT     specify listening port [default port is 12345]\n"
           "    -h, --help          display this help and exit\n", arg0);
    exit(ERR_FSERVER_DIRECTORY_MISSING);
}

void parse_args(int argc, char* argv[]) {
    if (argc == 1) {
        print_help_and_exit(argv[0]);
    }

    int opt;                // return value of getopt_long()
    int option_index = 1;   // skip the first argument which is the program name

    while ((opt = getopt_long(argc, argv, short_options_server, long_options_server, &option_index)) != -1) {
        switch (opt) {
            case 'l':
                if (strlen(optarg) > 0) {
                    options.hostname = optarg;      // points to argv[i] to main, safe to use in the program
                    break;
                } else {
                    fprintf(stderr, "Missing source address");
                    exit(ERR_FSERVER_ARGUMENT_MISSING);
                }
            case 'H':
                options.hidden = TRUE;
                break;
            case 'p':
                options.port = atoi(optarg);    // NOLINT: atoi is safe here
                if (options.port <= 0 || options.port > 65535) {
                    fprintf(stderr, "Invalid port number: %s", optarg);
                    exit(ERR_FSERVER_PORT_INVALID);
                }
                break;
            case 'h':
                print_help_and_exit(argv[0]);
                break;
            case 1:     // positional argument for DIR
                if (strlen(options.dirname) > 0) {
                    fprintf(stderr, "Can only serve one directory");
                    exit(ERR_FSERVER_DIRECTORY_RESPECIFIED);
                }
                options.dirname = optarg;
                options.dirname_len = strlen(optarg);
//                if (optarg[options.dirname_len - 1] == '/') {
//                    optarg[options.dirname_len - 1] = '\0';      // remove trailing separator if you want
//                    options.dirname_len -= 1;
//                }
                break;
            case '?':
                fprintf(stderr, "Unknown argument %s", argv[option_index]);
                exit(ERR_FSERVER_ARGUMENT_INVALID);
            default:
                fprintf(stderr, "Internal error, optarg is %s", optarg);
                exit(ERR_FSERVER_INTERNAL_ERROR);
        }
    }

    if (strlen(options.dirname) == 0) {
        fprintf(stderr, "Missing argument DIR");
        exit(ERR_FSERVER_DIRECTORY_MISSING);
    }
}

__attribute__((noreturn))
void do_fserver() {
    // start the server first
    start_server(128);

    int ret;    // hold the return value of some functions, indicating success or failure
    if ((ret = handle_list_files(STDOUT_FILENO)) != 0) {
        exit(ret);
    }

    char buf[BUF_SIZE];
    pthread_t tid;
    struct sockaddr_in client_addr;
    unsigned int addr_size = sizeof(struct sockaddr_in);
    while (TRUE) {
        connected_fd = accept(socket_fd, (void *) &client_addr, &addr_size);
        if (connected_fd > 0) {
            ret = pthread_create(&tid, NULL, handle_client_session, (void *) (long) connected_fd);
            print_line("[conn] Client from %s:%d, fd %d, handling by tid %lu",
                       inet_ntop(AF_INET, &client_addr.sin_addr, buf, INET_ADDRSTRLEN), client_addr.sin_port, connected_fd, tid);
            if (ret) {
                strcpy(buf, "e");
                strcat(buf, strerror(errno));
                send(connected_fd, buf, strlen(buf), 0);
                close(connected_fd);
            }
        } else {    // connected_fd = -1
            print_line_stderr("Failed to accept request: %s", strerror(errno));
        }
    }
}

int list_files(int out_fd, char **path_ptr, size_t pathlen, int layer, int depth) {    // NOLINT: intended recurrence
    if (layer > depth) { return 0; }
    char *path = *path_ptr;
    if (access(path, R_OK) == -1) { return ERR_FSERVER_ACCESS_DENINED; }
    struct stat statbuf;
    if (lstat(path, &statbuf) < 0) {
        print_line_stderr("Error: lstat error on path %s", path);
        return ERR_FSERVER_INTERNAL_ERROR;
    }
    if (!S_ISDIR(statbuf.st_mode)) {
        print_line_stderr("Error: %s is not a directory", path);
        return ERR_FSERVER_NOT_A_DIRECTORY;
    }

    if (layer == 0) {
        dprintf(out_fd, "File list:\n");
    }

    size_t n = strlen(path);
    if (pathlen < (n + NAME_MAX + 2)) {
        pathlen = MAX(n + NAME_MAX + 2, pathlen * 2);
        if ((*path_ptr = realloc(path, sizeof(char) * pathlen)) == NULL) {
            *path_ptr = path;
            return -1;
        }
        path = *path_ptr;
    }
    path[n] = '/';
    path[++n] = '\0';

    DIR *dir_p;
    struct dirent *dirent_p;

    dir_p = opendir(path);
    if (dir_p == NULL) {
        print_line_stderr("Error: Cannot open directory");
        return ERR_FSERVER_ACCESS_DENINED;
    }

    while ((dirent_p = readdir(dir_p)) != NULL) {
        if ((strcmp(dirent_p->d_name, "..") == 0) || (strcmp(dirent_p->d_name, ".") == 0)) {
            continue;
        }
        // show hidden file only when -H is set
        if (dirent_p->d_name[0] != '.' || options.hidden) {
            strcpy(&path[n], dirent_p->d_name);
            if (access(path, R_OK) == -1) { continue; }     // cannot access this file
            if (lstat(path, &statbuf) < 0) { continue; }        // lstat error
            // TODO why does send() not work with indent?
            write(out_fd, indent, (layer + 1) * 2);
            if (S_ISREG(statbuf.st_mode)) {
                dprintf(out_fd, "- %s\n", dirent_p->d_name);
            } else if (S_ISDIR(statbuf.st_mode)) {
                dprintf(out_fd, "+ %s\n", dirent_p->d_name);
                list_files(out_fd, path_ptr, pathlen, layer + 1, depth);
                path = *path_ptr;   // restore the path pointer
            }
        }
    }
    closedir(dir_p);
    return 0;
}

void send_file(int client_fd, char *filename) {
    struct stat statbuf;
    if (lstat(filename, &statbuf) < 0) {
        print_line_stderr("Error: failed to lstat file %s - %s", filename, strerror(errno));
        send_error_message(client_fd, "the file is not available");
        return;
    }
    if (!S_ISREG(statbuf.st_mode)) {
        send_error_message(client_fd, "%s is not a regular file", filename);
        return;
    }

    int fd;
    if ((fd = open(filename, O_RDONLY)) == -1) {
        print_line_stderr("Error: failed to open file %s - %s", filename, strerror(errno));
        send_error_message(client_fd, "failed to open the file in the server");
        return;
    }
    char buf[BUF_SIZE];
    send(client_fd, "s", 1, 0);
    send(client_fd, (char *) &statbuf.st_size, sizeof(off_t), 0);
    ssize_t bytes_read;
    while ((bytes_read = read(fd, buf, BUF_SIZE)) > 0) {
        send(client_fd, buf, bytes_read, 0);
    }
    if (bytes_read < 0) {
        print_line_stderr("Error: failed to read file %s - %s", filename, strerror(errno));
        send_error_message(client_fd, "failed to read file in the server");
    }
    close(fd);
}

int start_server(int qlen) {
    struct addrinfo template = { AI_ADDRCONFIG, AF_INET };
    struct addrinfo *server_ai_p;     // actually a linked list, here we only take the first usable element
    struct sockaddr_in *server_addr;
    int err;
    // TODO check the format of the address
    if ((err = getaddrinfo(options.hostname, NULL, &template, &server_ai_p) != 0)) {
        print_error_and_exit(ERR_FSERVER_INTERNAL_ERROR, "Fatal: getaddrinfo failed - %s", gai_strerror(err));
    }
    if (!server_ai_p) {
        print_error_and_exit(ERR_FSERVER_INTERNAL_ERROR, "Fatal: getaddrinfo does not return any result");
    }
    server_addr = (struct sockaddr_in *) server_ai_p->ai_addr;
    server_addr->sin_port = options.port;

    const int REUSE = 1;
    if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        print_error_and_exit(ERR_FSERVER_INTERNAL_ERROR, "Fatal: create socket failed");
    }
    if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &REUSE, sizeof(int)) < 0) {
        goto catch_error;
    }
    if (bind(socket_fd, (struct sockaddr *) server_addr, sizeof(struct sockaddr_in)) < 0) {
        goto catch_error;
    }
    if (listen(socket_fd, qlen) < 0) {
        goto catch_error;
    }
    char addr_str[INET_ADDRSTRLEN];
    print_line("Server listening at %s:%d", inet_ntop(AF_INET, &server_addr->sin_addr, addr_str, INET_ADDRSTRLEN), server_addr->sin_port);

    // don't forget to free the memory before return
    freeaddrinfo(server_ai_p);
    return socket_fd;

catch_error:
    err = errno;
    close(socket_fd);
    errno = err;
    print_error_and_exit(ERR_FSERVER_INTERNAL_ERROR, "Fatal: %s", strerror(errno));
}

void *handle_client_session(void *data) {
    int fd = (int) (long) data;     // it is safe to downcast to int
    char command;
    if (recv(fd, &command, sizeof(char), 0) > 0) {
        switch (command) {
            case 'd':
                handle_download_file(fd);
                close(fd);
                break;
            case 'l':
                handle_list_files(fd);
                close(fd);
                break;
            default:
                send_error_message(fd, "invalid command");
        }
    } else {
        print_line_stderr("[sess] Unable to receive data from client of socket %d", fd);
        return (void *) 1;
    }
    return (void *) 0;
}

int handle_list_files(int out_fd) {
    size_t pathlen = options.dirname_len + 1;
    char *path = (char *) malloc(sizeof(char) * (pathlen));
    strcpy(path, options.dirname);
    int res = list_files(out_fd, &path, pathlen, 0, MAX_REC_DEPTH);
    free(path);
    return res;
}

void handle_download_file(int client_fd) {
    size_t filename_size;
    if (recv(client_fd, (char *) &filename_size, sizeof(size_t), 0) > 0) {
        size_t buf_len = options.dirname_len + 1 + filename_size + 1;   /* base / filename \0 */
        if (filename_size > FILENAME_MAX * (MAX_REC_DEPTH + 1)) {
            send_error_message(client_fd, "filename is too long");
            return;
        }
        char fullpath[buf_len];
        strcpy(fullpath, options.dirname);
        fullpath[options.dirname_len] = '/';
        fullpath[buf_len - 1] = '\0';
        if (recv(client_fd, &fullpath[options.dirname_len + 1], filename_size, 0) == filename_size) {
            print_line("[sess] sending file %s to client with fd %d", fullpath, client_fd);
            send_file(client_fd, fullpath);
        } else {
            send_error_message(client_fd, "invalid filename");
        }
    } else {
        send_error_message(client_fd, "invalid command");
    }
}
